<?php

1->服务网关在微服务中的作用
   -) '对外服务的难题'
       微服务架构下的应用系统体系很庞大，光是需要独立部署的基础组件就有注册中心、配置中心和服务总线、Turbine异常聚合和监控大盘、调用链追踪器和链路聚合，
       还有Kafka和MQ之类的中间件，再加上拆分后的零散微服务模块，一个小系统都能轻松弄出20个左右的部署包.

       我们前面都是采用'localhost加端口'的方式直接访问，如果这些服务一并都要提供给外部用户访问那该怎么办呢？
       产品经理表示可以让前端程序员加班加点在各个页面给各种不同请求配置URL和端口号，人不是问题，项目完成就行。可是这一大堆URL在页面上换来换去的，
       用户还以为是进了钓鱼网站。 有人会说，我们配一个URL，通过F5或者Nginx可以做路由，话是没错，可是这样就要让运维团队手工维护路由规则表，
       当我们新增删除节点或者因为更换机房导致IP变化的时候就很麻烦。因此我们需要引入一套机制来降低路由表的维护成本.

       还有一个问题就是安全性，我们在提供外部服务的时候往往会加入一些访问控制，比如说下单接口不允许未登录用户的访问，
       有的服务还会通过一些JWT签名等防止客户端篡改数据。如果让每个服务提供者都实现同样的访问验证逻辑未免有些太繁琐，这样纯属是增加研发人员的怒气值，
       况且如果有一天我们需要更换权限认证方案，比如更换为OAuth2.0，难不成还要每个服务提供者都做变更?

       我们如何对外提供服务，既能管好路由规则，还能做好访问控制呢？在这个背景下，API网关应运而生，它就像一个传达室的角色，接待所有来访请求.

       总结: 随着服务节点越来越多, 我们应该怎么对外暴露接口地址? 又怎么保证接口安全? 这是个'急需解决'的问题!!!

   -) '微服务传达室'
       在计算机领域，任何问题都可以通过引入一个中间层来解决。接下来，我们就给微服务引入一层专事专办的中间层-传达室.
       . 访问控制: 看你是不是有权限进行访问，拒绝未授权的来访者.
       . 引导指路: 问清楚你要办的事儿，给指条明路，找到对应处理这些事儿的人.

                                    HTTP                          HTTP
                                     ||                            ||
                           +--------------------------------------------------+
                           |                      服务网关                     |
                           +--------------------------------------------------+
                                    |                |               |
                                    |           +-------------------------+
                              +----------+      |                         |
                              |  Eureka  | <--- |    服务           服务   |
                              +----------+      |                         |
                                                |    服务           服务   |
                                                +-------------------------+

      网关层作为唯一的对外服务，外部请求不直接访问服务层，由'网关层承接所有HTTP请求'。在实际的应用里，我们也会将'Gateway + Nginx'一同使用。
      接下来，我们分别就[访问控制]和[路由规则]这两个方面做些介绍。

   -) '访问控制'
       访问控制主要包含两个方面的任务，具体实现并不是由网关层提供，但是网关作为一个载体承载了这两个任务:
       . 拦截请求: 有的接口需要登录用户才能访问，对这类接口的访问，网关层可以检查访问请求中是否携带'令牌'等身份信息，比如HTTP Header中的'Authorization'
                  或者'token'属性。如果没有携带令牌，则说明还没有登录，这时可以直接返回'403 Forbidden'.

       . 鉴权: 对于携带有令牌的服务，我们需要验证令牌的真假，否则用户可以通过伪造的令牌进行通信。对令牌校验失败的服务请求，或者令牌已过期的请求执行拒绝服务!

   -) '路由规则'
       路由规则包含两个方面，分别是URL映射和服务寻址:
       . URL映射:
         在大多数情况下，客户端访问的HTTP 'URL往往不是我们在Controller里配置的真实路径'，比如客户端可以发起请求'/password/update'来修改密码，
         但后台并没有这个服务，这时候就需要网关层做一个路由规则，将来访URL映射成真正的服务路径，
         比如将刚才的密码修改请求的路径映射到"/user/settings/security/password"请求.
         (核心:  '请求路径 -----> 服务内部路径')

       . 服务寻址:
         URL映射好了之后，网关层就需要找到可以提供服务的服务器地址，对于服务集群的话，还需要实现负载均衡策略。
         (在Spring Cloud中，Gateway是借助Eureka的服务发现机制来实现服务寻址的，负载均衡则依靠Ribbon).


2->为什么是Gateway?
   -) '能做什么?'
                                 路由寻址
                                    |
                              +-----+-----+
              负载均衡 <------ |  Gateway  | ------> 鉴权
                              +-----+-----+
                                    |
                                   限流 (第一道关就砍掉一部分)

   -) 'Gateway与Zuul不得不说的那些事'
       +==============+==============+==============+==============+
       |              |    Gateway   |    Zuul1.x   |    Zuul2.x   |
       |==============+==============+==============+==============+
       |    可靠性          官方支持       曾经靠谱过       专业放鸽子   |
       |--------------+--------------+--------------+--------------|
       |     性能            Netty       同步阻塞,慢        Netty    |
       |--------------+--------------+--------------+--------------|
       |     RPS           >32000        20000左右       25000左右  |
       |--------------+--------------+--------------+--------------|
       | Spring Cloud      已整合           已整合       暂无整合计划  |
       |--------------+--------------+--------------+--------------|
       |     长连接          支持           不支持           支持     |
       |--------------+--------------+--------------+--------------|
       |    编程体验          略难          容易上手          略难     |
       |--------------+--------------+--------------+--------------|
       |  调试/链路追踪       略难           无压力           略难     |
       +--------------+--------------+--------------+--------------+


3->Gateway体系架构解析
   前面我们了解了Spring Cloud中的第二代网关Gateway，作为网二代来说它在各项指标上都领先Zuul，实在没有理由不在新项目中用Gateway。
   那么, 我们就去看看Gateway的体系架构，看看它到底好在哪里...
   -) '网二代的家底'
       所有二代都有同样的特点，家底子厚实。我们Gateway也不例外，打开Gateway的自动装配工厂GatewayAutoConfiguration来看一下，
       排头第一个类赫然写着五个金光闪闪的大字'Netty'.

       Netty是什么? 在网络传输领域Netty就是'身份的象征',黄金AK般的存在, 它是非阻塞, 高性能, 高可靠的'异步输入输出框架', 用一个字概括就是'快'!
       Netty在Gateway中主要应用在以下几个地方:
       . 发起服务调用: 由'NettyRoutingFilter'过滤器实现，底层采用基于Netty的HttpClient发起外部服务的调用.

       . Response传输: 由'NettyResponseFilter'过滤器实现，网络请求结束后要将Response回传给调用者.

       . Socket连接: 具体由'ReactorNettyWebSocketClient'类承接，通过Netty的HttpClient发起连接请求.

       在Gateway中发起Request和回传Response之类的步骤都是通过一系列过滤器完成的.

                                                +-----------+
       +--------------------------------------- | Client请求 | <---------------------------------------+
       |                                        +-----------+                                         |
       |                                                                                              |
       |                  +---------------------------------------------------------+                 |
       |                  |                        服务网关                          |                 |
       |                  |                                                         |                 |
       +--------> NettyRoutingFilter --------> HttpClient(Netty) --------> NettyResponseFilter -------+
                                                     ||
                                                     ||
                                                     ||
                                            +------------------+
                                            | 服务   服务   服务 |
                                            +------------------+

       Client发起请求到服务网关之后，由NettyRoutingFilter底层的HttpClient (也是Netty组件) 向服务发起调用,
       调用结束后的Response由NettyResponseFilter再回传给客户端。有了Netty的加持，网络请求效率大幅提升 (Zuul 1.x还是使用Servlet，在2.x版本才移步到Netty)
       由此可见: Netty贯穿了从Request发起到Response结束的过程，'承担了所有和网络调用相关的任务'.

   -) 'Gateway自动装配'
       都是加载什么资源???
                                   +--------------+
                                   |    Gateway   |
                                   +-------+------+
                                           |
                +-----------------+-----------------+-----------------+
                |                 |                 |                 |
            AutoConfig    LoadBalancerClient  ClassPathWarning      Redis

       . [AutoConfig]
          作为核心自动装配主类，GatewayAutoConfiguration负责初始化所有的Route路由规则、 Predicate断言工厂和Filter
          (包括Global Filter和Route Filter), 这三样是Gateway吃饭的家伙，用来完成路由功能。AutoConfig也会同时加载Netty配置.

       . [LoadBalancerClient]
          这部分在AutoConfig完成之后由GatewayLoadBalancerClientAutoConfiguration负责加载，用来加载Ribbon和一系列负载均衡配置.

       . [ClassPathWarning]
          同样也是在AutoConfig完成之后触发 (具体加载类为GatewayClassPathWarningAutoConfiguration),
          由于Gateway底层依赖Spring WebFlux的实现，所以它会检查项目是否加载了正确配置.

       . [Redis]: 在Gateway中Redis主要负责限流的功能.

   -) '不太重要的类'
       除了上面几个核心装配工厂以外，还有两个打酱油的路人，它们并不直接参与Gateway的核心功能，但是会提供一些重要的支持功能，我们也来一道认识下:
       . GatewayMetricsAutoConfiguration: 负责做一些统计工作，比如对所谓的“short task”运行时长和调用次数做统计.
       . GatewayDiscoveryClientAutoConfiguration: 服务发现客户端自动装配类.

   -) '路由流程'
       这里就涉及到了Gateway最核心的路由功能，路由主要由'断言 + 过滤器'配合来实现.

   -) '踩坑注意'
       如果Gateway项目启动出错，但是查来查去，发现也没有什么配置问题。这时候就要看一下是不是引入了错误的依赖，
       Gateway比较坑的一个地方是它基于'WebFlux'实现，因此它需要的依赖是'spring-boot-starter-webflux'!
       假如不小心引入了spring-boot-starter-web将导致启动问题.

       由于我们大部分的Spring Cloud项目都依赖spring-boot-starter-web，所以很容易就误将其依赖导入到了Gateway项目中，
       碰到这种问题只要打印出依赖树，排查下错误依赖的来源，然后将它在pom中排除出去就好了 ^-^


4->使用
   -) 'pom'
       <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-gateway</artifactId>       # 里面包含: [spring-boot-starter-webflux]
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
        </dependencies>

   -) '配置文件'
       spring:
         application:
           name: gateway-consumer
         cloud:
           gateway:
             discovery:
               locator:
                 enabled: true                      # 是否开启从注册中心获取其余服务
                 lower-case-service-id: true        # 转 [小写] 调用其余服务名


5->Route规则
   -) 'postman'
       - '查看 - routes - GET'
          ip:port/actuator/gateway/routes

       - '新增 - route - POST'
          ip:port/actuator/gateway/routes/{route-id}
          {
              "predicates": [
                  {
                      "name": "Path",
                      "args": {
                          "_genkey_0" : "/xm/**"
                      }
                  }
              ],
              "filters": [
                  {
                      "name": "StripPrefix",
                      "args": {
                          "_genkey_0" : "1"
                      }
                  }
              ],
              "uri": "lb://FEIGN-CONSUMER",             # lb: loadbrancher
              "order": 0
          }
       说明: 添加此route之后, 凡是请求以: /xm/... 的url都会被gateway转发到 ['FEIGN-CONSUMER']

       - '删除 - route - DELETE'
          ip:port/actuator/gateway/routes/{route-id}


6->路由
   前面我们落地了一个自动路由的网关服务，这一节我们就去看一看Gateway中的路由是怎么工作的。
   Gateway网关的路由功能可不是简简单单的'转发'请求，在请求到达网关再流转到指定服务之间发生了很多事儿，它不光可以拒绝请求，
   甚至可以'篡改'请求的参数，我们接下来就去看看路由这里面的门道。
   -) '路由三重门'
                              +-----------+
                              |  断言集合  |
                              +-----+-----+
                                    |
                                +-------+
                                | Route |
                                +---+---+
                                    |
                      +---------------------------+
                      |                           |
                  过滤器集合                       URI

       Gateway中可以定义很多个Route，一个Route就是一套包含完整转发规则的路由，主要由三部分组成:
       . 断言集合: 断言是路由处理的第一个环节，它是路由的匹配规则，它决定了一个网络请求是否可以匹配给当前路由来处理。
                 之所以它是一个集合的原因是我们可以给一个路由添加多个断言，当每个断言都匹配成功以后才算过了路由的'第一关'.

       . 过滤器集合: 如果请求通过了前面的断言匹配，那就表示它被当前路由正式接手了，接下来这个请求就要经过一系列的过滤器集合。过滤器的功能就是八仙过海各显神通了，
                   可以对当前请求做一系列的操作，比如说权限验证，或者将其他非业务性校验的规则提到网关过滤器这一层。
                   在过滤器这一层依然可以通过修改Response里的Status Code达到中断效果，比如对鉴权失败的访问请求设置'Status Code为403'之后中断操作。

       . URI: 如果请求顺利通过过滤器的处理，接下来就到了最后一步，那就是转发请求。URI是统一资源标识符，它可以是一个具体的网址，也可以是'IP+端口'的组合,
              或者是Eureka中注册的服务名称

   -) '关于负载均衡'
       对最后一步寻址来说，如果采用基于Eureka的服务发现机制，那么在Gateway的转发过程中可以采用服务注册名的方式来调用，后台会借助Ribbon实现负载均衡
       （可以为某个服务指定具体的负载均衡策略），其配置方式如:
       lb://FEIGN-SERVICE-PROVIDER             前面的lb就是指代Ribbon作为LoadBalancer.

   -) '路由工作流程'
       当一个请求到达网关开始, Gateway内部流转的过程.

                                          +----------------+
                                          |     Request    |
                                          +--------+-------+
                                                   |
                              +--------------------+--------------------+
                              |                    |                    |
                              |        +-----------+----------+         |
                              |        |   Predicate Handler  |         |
                              |        +-----------+----------+         |
                              |                    |                    |
                              |        +-----------+----------+         |
                      +--------------- |    Filter Handler    | <---------------+
                      |                +-----------+----------+                 |
                      |                                                         |
                      |                                                         |
                      |     +------------+                   +------------+     |
                      +---->| Pre Filter |                   | Post Filter|-----+
                            +------------+                   +------------+
                                  |                                 |
                                  |         +-------------+         |
                                  +-------->|   Service   |--------->
                                            +-------------+

       . Predicate Handler:
         具体承接类是RoutePredicateHandlerMapping。首先它获取所有的路由(配置的routes全集), 然后依次循环每个Route，
         把应用请求与Route中配置的所有断言进行匹配，如果当前Route所有断言都验证通过，Predict Handler就选定当前的路由。这个模式是典型的职责链。

       . Filter Handler:
         在前一步选中路由后，由FilteringWebHandler将请求交给过滤器，在具体处理过程中，不仅当前Route中定义的过滤器会生效，
         我们在项目中添加的全局过滤器 (Global Filter) 也会一同参与。Pre Filter和Post Filter，这是指'过滤器的作用阶段'.

       . 寻址: 这一步将把请求转发到URI指定的地址，在发送请求之前，所有Pre类型过滤器都将被执行，而Post过滤器会在调用请求返回之后起作用.


7->断言 (我一眼就看出你不对劲...)
   这个功能决定了一个HTTP请求应该由哪个Route来做路由.
   -) 'Predicate机制'
       Predicate是Java 8中引入的一个新功能，就和我们平时在项目中写单元测试时用到的Assertion差不多，Predicate接收一个判断条件，
       返回一个ture或false的布尔值结果，告知调用方判断结果。你也可以通过 and(与), or(或) 和 negative (非) 三个操作符将多个Predicate串联在一块共同判断。

       前面说到Gateway是挡在微服务前面的传达室大爷，那这个Predicate就是和大爷的接头暗号。比如大爷可以要求你的Request中必须带有某个指定的参数叫name，
       对应的值必须是一个指定的人名“马冬梅”，如果你的Request中没有包含name，或者对应的人名给成了“马北梅”，那就是断言失败。
       只有当你的'请求完全和接头暗号匹配'的时候，大爷才能给你放行。

       说白了: Predicate就是一种路由规则，通过Gateway中丰富的内置断言的组合，我们就能让一个请求找到对应的Route来处理。

   -) '断言的作用阶段 --- 拜码头'
       一个请求在抵达网关层后，首先就要进行断言匹配，在满足所有断言之后才会进入Filter阶段。有关Filter的内容将在接下来的小节内详细介绍。

   -) '常用断言'
       - [路径断言]
          .route(r -> r.path("/gateway/**")
                      .uri("lb://FEIGN-SERVICE-PROVIDER/")
          )
          .route(r -> r.path("/baidu")
                       .uri("http://baidu.com:80/")
          )

       - [Method断言]
          .route(r -> r.path("/gateway/**")
                       .and().method(HttpMethod.GET)
                       .uri("lb://FEIGN-SERVICE-PROVIDER/")
          )

       - [RequestParam断言]
          .route(r -> r.path("/gateway/**")
                       .and().method(HttpMethod.GET)
                       .and().query("name", "test")         # 请求列表中必须包含[name => test]
                       .and().query("age")                  # 请求参数必须包含[age], 值有没有无所谓
                       .uri("lb://FEIGN-SERVICE-PROVIDER/")
          )

       - [Header断言 - 校验token]
          .route(r -> r.path("/gateway/**")
                       .and().header("Authorization")
                       .uri("lb://FEIGN-SERVICE-PROVIDER/")
          )

       - [Cookie断言]
          .route(r -> r.path("/gateway/**")
                       .and().cookie("name", "test")        # cookie必须校验 [k-v] 一对
                       .uri("lb://FEIGN-SERVICE-PROVIDER/")
          )

       - [时间片断言]
          .route(r -> r.path("/gateway/**")
                       .and().before(ZonedDateTime.now().plusMinutes(1))
                       .uri("lb://FEIGN-SERVICE-PROVIDER/")
          )
          说明: ZonedDateTime.now().plusMinutes(1)表示当前时间的后一分钟，由于路由的规则是在项目启动时加载的，
               那么这里的当前时间也就是项目加载完成的时间，因此该路由的有效时间就是服务启动后的一分钟以内。

          时间匹配有三种模式: 分别是[Before / After / Between], 这些断言指定了在什么'时间范围内'路由才会生效!

   -) '自定义断言'
       Gateway也提供了一个扩展方法，用来将自定义的断言应用到路由上。老师给出两点提示，希望同学们顺着这个方向来参考Gateway的源码，
       实现一个自定义断言，完成一个小功能：将所有请求参数大于5个的访问请求拦截掉，即RequestParam个数小于5个的请求才能被放行。


8->过滤器原理和生命周期
   -) '过滤器 - 工作模式'
       所有开源框架实现过滤器的模式都是大同小异，通过一种类似职责链的方式，传统的职责链模式中的事件会传递直到有一个处理对象接手，而过滤器和传统的职责链有点不同，
       它更像是足球队开场握手一样，所有队员一字排开，你要从头到尾'依次'和所有球员握过手。

       Gateway中的过滤器也是一样的模型，他们经过优先级的排列，所有网关调用请求从最高优先级的过滤器开始，一路走到头，直到'被最后一个过滤器处理'.

   -) '过滤器 - 实现方式'
       在Gateway中实现一个过滤器非常简单，只要'实现GatewayFilter'接口的默认方法就好了...

       @Override
       public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
           // 随意发挥
           return chain.filter(exchange);
       }

       . ServerWebExchange: 这是Spring封装的HTTP request-response交互协议，从中我们可以获取request和response中的各种请求参数，也可以向其中添加内容.
       . GatewayFilterChain: 它是过滤器的调用链，在方法结束的时候我们需要将exchange对象传入调用链中的下一个对象.

   -) '过滤器 - 执行阶段'
       比对Zuul:
       不同于Spring Cloud中上一代网关组件Zuul里对过滤器的Pre和Post的定义，Gateway是通过Filter中的代码来实现'类似'Pre和Post的效果。

       Pre和Post是指代当前过滤器的执行阶段，Pre是在下一个过滤器之前被执行，Post是在过滤器执行过后再执行。我们在Gateway Filter中也可以同时定义Pre和Post执行逻辑。

       - 'Pre类型'
          拿AddResponseHeaderGatewayFilterFactory举例，它可以向Response中添加Header信息:

          @Override
          public GatewayFilter apply(NameValueConfig config) {
         	 return (exchange, chain) -> {
                 exchange.getResponse().getHeaders().add(config.getName(), config.getValue());
                 return chain.filter(exchange);
              };
          }

          说明: 这里的具体执行方法是定义在调用“chain.filter()”方法之前，也就是在转发到下级调用链路之前执行的，因此可以理解为一个Pre类型的过滤器.

       - 'Post类型'
          拿SetStatusGatewayFilterFactory举例, 它在过滤器执行完毕之后, 将制定的HTTP status返回给调用方:

          return chain.filter(exchange).then(Mono.fromRunnable(() -> {
       	     // 这里是业务逻辑 ...
       	 }));

          这个过滤器的主要逻辑在then方法中，then是一个回调函数，在下级调用链路都完成以后再执行，因此这类过滤器可以看做是Post Filter.

   -) '过滤器 - 排座次'
       在Gateway中我们可以通过实现org.springframework.core.Ordered接口, 来给过滤器指定执行顺序, 比如下面的代码实现了Ordered接口方法,
       将过滤器执行顺序设置为0:
       @Override
       public int getOrder() {
       	return 0;
       }
       注意:
       Pre类型: 数字越大 -> 越先被执行;
       Post类型: 数字越小 -> 越先被执行.

   -) '过滤器 - 示例'
       - [header]
          这个系列有很多组过滤器，AddRequestHeader和AddResponseHeader，分别向Request和Response里加入指定Header。
          相应的RemoveRequestHeader和RemoveResponseHeader分别做移除操作，用法也很简单:

          .filters(f -> f.addResponseHeader("who", "gateway-header"))

       - [StripPrefix]
          这是个比较常用的过滤器，它的作用是去掉部分URL路径。比如我们的过滤器配置如下:

          .route(r -> r.path("/gateway-test/**")
                       .filters(f -> f.stripPrefix(1))
                       .uri("lb://FEIGN-SERVICE-PROVIDER/")
          )

          说明:
          假如HTTP请求访问的是'/gateway-test/sample/update',
          如果没有StripPrefix过滤器，那么转发到[FEIGN-SERVICE-PROVIDER]服务的访问路径也是一样的. 当我们添加了这个过滤器之后,
          Gateway就会根据 [stripPrefix(1)] 中的值截取URL中的路径，比如这里我们设置的是1，那么就去掉一个前缀,
          最终发送给后台服务的路径变成了'/sample/update'.

       - [PrefixPath]
          它和StripPrefix的作用是完全相反的，会在请求路径的前面加入前缀:
          .route(r -> r.path("/gateway-test/**")
                       .filters(f -> f.prefixPath("go"))
                       .uri("lb://FEIGN-SERVICE-PROVIDER/")
          )

          说明:
          比如说我们访问'/gateway-test/sample'的时候，上面例子中配置的过滤器就会把请求发送到'/go/gateway-test/sample'.

       - [RedirectTo]
          它可以把收到特定状态码的请求重定向到一个指定网址:
          .filters(f -> f.redirect(302, "https://www.xm.com/"))

          说明:
          上面的例子接收HTTP status code和URL两个参数，如果请求结果是302，则重定向到第二个参数指定的页面，这个功能也可以做统一异常处理，
          将Unauthorized或Forbidden请求重定向到登录页面.



9->路由之————牛刀小试(双刀流)
   -) '配置文件'
       spring:
         application:
           name: gateway-consumer
         cloud:
           gateway:
             discovery:
               locator:
                 enabled: true
                 lower-case-service-id: true
             routes:
             - id: feign-consumer        # -: 代表数组, 指定route名称
               uri: lb://FEIGN-CONSUMER  # 通过后转发的uri
               predicates:               # 断言
               - Path=/yml/**            # 凡是以这种格式             **/
               filters:
               - StripPrefix=1           # 将路径后的【第一个】截断，然后拼上转发的地址: /FEIGN-CLIENT/**

   -) '配置类'
       @Configuration
       public class GatewayConfig {

          @Bean
          @Order
          public RouteLocator customizedRoutes(RouteLocatorBuilder builder) {
              return builder.routes()
                      .route(r -> r.path("/java/**")
                              .and().method(HttpMethod.GET)
                              .and().header("name")
                              .filters(f -> f.stripPrefix(1)
                                      .addResponseHeader("java-param", "My name is Gateway-Route"))
                              .uri("lb://FEIGN-CONSUMER")
                      ).build();
          }
       }

   -) '比较'
       route: 进行各种'判断过滤'!
       filter: 可以'对请求搞小动作'.


10->借助Gateway实现秒杀
   -) '思路'
       . 添加秒杀路径断言
       . after（ZoneDateTime），只在某个时间后生效
       . before 在某个时间前
       . between 在某个时间之间

   -) '配置类'
       @Bean
       @Order
       public RouteLocator customizedRoutes(RouteLocatorBuilder builder) {
           return builder.routes()
           .route(r ->r.path("/seckill/**").and().after(ZonedDateTime.now().plusMinutes(1)).filters(f -> f.stripPrefix(1)).uri("lb://FEIGN-CONSUMER"))
           // 商品服务
           .route(r -> r.path("/items/**").uri("lb://FOODIE-ITEM-SERVICE"))
           // 商家服务
           .route(r -> r.path("/shopcart/**").uri("lb://FOODIE-CART-SERVICE"))
           // 订单服务
           .route(r -> r.path("/orders/**", "/myorders/**", "/mycomments/**").uri("lb://FOODIE-ORDER-SERVICE"))
           .build();
       }


11->自定义过滤器 (详见'java/MyFilter.java + GatewayConfig.java')


12->网关的个人理解
    可以通过配置'routes'规则, 将前端请求过来的路径(其实也就是我们各个服务对外暴露的接口地址), 经由'gateway'层的匹配,
    转发到'各自对应'的服务路径, 匹配真实的后端接口地址!!!

    不过: 一般网关勾搭Nginx做转发处理, 通过Ng配置转发到各自服务之间的负载均衡.('软负载')

    ================================== Nginx
    - 'nginx.conf'
    upstream roadshow-activity {

      server 10.60.10.32:30003;
      server 10.60.10.92:30003;
      server 10.60.10.124:30003;
    }

    location /api/roadshow-activity/ {
      proxy_redirect off;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      rewrite ^/api/(.*)$ /api/$1 break;
      proxy_pass http://roadshow-activity;
    }

    ================================== Gateway
    @Bean
    @Order
    public RouteLocator customizedRoutes(RouteLocatorBuilder builder) {
        return builder.routes()
                .route(r -> r.path("/seckill/**")
                        .and().after(ZonedDateTime.now().plusMinutes(1))
                        .filters(f -> f.stripPrefix(1))
                        .uri("lb://FEIGN-CONSUMER")
                )
                .build();
    }


13->权限的辛酸之路————原始
    -) '传统单应用的用户鉴权'
        从我们开始学JavaEE的时候，就被洗脑式灌输了一种权限验证的标准做法，那就是将用户的登录状态保存到HttpSession中，
        比如在登录成功后保存一对key-value值到session，key是userId而value是用户后台的真实ID。接着创建一个ServletFilter过滤器，
        用来拦截需要登录才能访问的资源，假如这个请求对应的服务端session里找不到userId这个key，那么就代表用户尚未登录，这时候可以直接拒绝服务然后重定向到用户登录页面。

        大家应该都对session机制比较熟悉，它和cookie是相互依赖的，cookie是存放在用户浏览器中的信息，而session则是存放在服务器端的。
        当浏览器发起服务请求的时候就会带上cookie，服务器端接到Request后根据cookie中的jsessionid拿到对应的session。

        由于我们只启动一台服务器，所以在登录后保存的session始终都在这台服务器中，可以很方便的获取到session中的所有信息。用这野路子，
        我们一路搞定了各种课程作业和毕业设计。结果一到工作岗位发现行不通了，因为所有应用都是集群部署，在一台机器保存了的session无法同步到其他机器上。
        那我们有什么成熟的解决方案吗？

    -) '分布式环境下的解决方案'
        - '同步session'
           Session复制是最容易先想到的解决方案，我们可以把一台机器中的session复制到集群中的其他机器。比如Tomcat中也有内置的session同步方案，
           但是这并不是一个很优雅的解决方案，它会带来以下两个问题:
           . Timing问题: 同步需要花费一定的时间，我们无法保证session同步的及时性，也就是说，当用户发起的两个请求分别落在不同机器上的时候，
                        前一个请求写入session的信息可能还没同步到所有机器，后一个请求就已经开始执行业务逻辑了，这不免引起'脏读幻读'.

           . 数据冗余: 所有服务器都需要保存一份session全集，这就产生了大量的冗余数据.

        - '反向代理: ip绑定 / 一致性hash'
           这个方案可以放在Nignx网关层做的，我们可以指定某些IP段的请求落在某个指定机器上，这样一来session始终只存在一台机器上。
           不过相比前一种session复制的方法来说，绑定IP的方式有更明显的缺陷:
           . 负载均衡: 在绑定IP的情况下无法在网关层应用负载均衡策略，而且某个服务器出现故障的话会对指定IP段的来访用户产生较大影响。
                     对网关层来说该方案的路由规则配置也极其麻烦!

           . IP变更: 很多网络运营商会时不时切换用户IP，这就会导致更换IP后的请求被路由到不同的服务节点处理，这样一来就读不到前面设置的session信息了.

           为了解决第二个问题, 可以通过一致性Hash的路由方案来做路由，比如根据用户ID做Hash，不同的Hash值落在不同的机器上，保证足够均匀的分配，
           这样也就避免了IP切换的问题，但依然无法解决第一点里提到的负载均衡问题。

        - 'Redis'
           这个方案解决了前面提到的大部分问题，session不再保存在服务器上，取而代之的是保存在redis中，所有的服务器都向redis写入/读取缓存信息。

           在Tomcat层面，我们可以直接引入tomcat-redis-session-manager组件，将容器层面的session组件替换为基于redis的组件，但是这种方案和'容器绑定的比较紧密'。
           另一个更优雅的方案是借助spring-session管理redis中的session，尽管这个方案脱离了具体容器，但依然是基于Session的用户鉴权方案，
           这类Session方案已经在微服务应用中被淘汰了。


14->权限的辛酸之路————升级版
    -) 'OAuth2.0'
        OAuth 2.0是一个开放授权标准协议，它允许用户让第三方应用访问该用户在某服务的特定私有资源，但是不提供账号密码信息给第三方应用。
        在上面的例子中，微信就相当于一个第三方应用，我们通过OAuth 2.0

        Auth Grant 在这一步Client发起Authorization Request到微信系统（比如通过微信内扫码授权），当身份验证成功后获取Auth Grant
        Get Token 客户端拿着从微信获取到的Auth Grant，发给第三方引用的鉴权服务，换取一个Token，这个Token就是访问第三方应用资源所需要的令牌
        访问资源 最后一步，客户端在请求资源的时候带上Token令牌，服务端验证令牌真实有效后即返回指定资源
        我们可以借助Spring Cloud中内置的 ['spring-cloud-starter-oauth2'] 组件搭建OAuth 2.0的鉴权服务,
        OAuth 2.0的协议还涉及到很多复杂的规范，比如角色、客户端类型、授权模式等...

    -) 'JWT - Json Wen Token'
        JWT也是一种基于Token的鉴权机制，它的基本思想就是通过用户名+密码换取一个Access Token.
        - '鉴权流程'
           1. 用户名+密码访问鉴权服务:
              . 验证通过: 服务器返回一个'Access Token'给客户端，并将token保存在服务端某个地方用于后面的访问控制（可以保存在数据库或者Redis中）
              . 验证失败: 不生成Token.

           2. 客户端使用令牌访问资源，服务器验证令牌有效性:
              . 令牌错误或过期: 拦截请求，让客户端重新申请令牌.
              . 令牌正确: 允许放行.

        - 'Access Token中的内容'
           JWT的Access Token由三个部分构成，分别是Header、Payload和Signature，我们分别看下这三个部分都包含了哪些信息:
           . Header: 头部声明了Token的类型（JWT类型）和采用的加密算法 (HS256).
             {
                'typ': 'JWT',
                'alg': 'HS256'
             }

           . Payload: 这一段包含的信息相当丰富，你可以定义Token签发者、签发和过期时间、生效时间等一系列属性，还可以添加自定义属性。
                      服务端收到Token的时候也同样可以对Payload中包含的信息做验证，比如说某个Token的签发者是“Feign-API”，
                      假如某个接口只能允许“Gateway-API”签发的Token，那么在做鉴权服务时就可以加入Issuer的判断逻辑。

           . Signature: 它会使用Header和Payload以及一个密钥用来生成签证信息，这一步会使用Header里我们指定的加密算法进行加密.


15->使用
    -) '整合web'
        注意: gateway依赖于'spring-boot-strater-webflux', 所以在整合别的微服务时, 如果别的有依赖'spring-boot-starter-web'的话,
             一定要手动排除!
             <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-gateway</artifactId>
                <exclusions>
                    <exclusion>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-web</artifactId>
                    </exclusion>
                </exclusions>
             </dependency>

    -) '自定义过滤器'
        使用自定义过滤器, 拦截'request'中是否存在'token', 不存在的话, 直接拒绝访问!
        详见'java/AuthFilter.java'



16->限流 (Gateway内置)
    -) '结合redis - 详见-/java/RedisLimiterConfiguration.java'

    -) '使用'
        . 配置Redis的'限流配置类'
        . 将配置好的类添加进'Route'

    -) '配置文件'
        spring:
          application:
            name: platform-gateway
          redis:
            host: localhost
            port: 6379
            database: 0
            password: xm
          main:
            allow-bean-definition-overriding: true
          cloud:
            gateway:
              globalcors:
                cors-configurations:
                  '[/**]':
                    # 返回的资源共享给请求来源
                    allowed-origins:
                    - "http://localhost:8080"
                    - "http://shop.z.mukewang.com:8080"
                    - "http://center.z.mukewang.com:8080"
                    - "http://shop.z.mukewang.com"
                    - "http://center.z.mukewang.com"
                    - "*"
                    # 比如cookie, authorization之类的认证信息
                    allow-credentials: true
                    allowed-headers: "*"
                    allowed-methods: "*"
                    expose-headers: "*"
                    # 表示Options可以在浏览器缓存多长时间
                    max-age: 600



17->感想
    - 1
      微服务一般以'服务[微 / 多]'而得名, 各个微服务模块都有自己的端口号, 那么前端不可能在请求时指定'ip:port'的,
      前端只会根据后台接口地址请求后台模块, 那么就'急需'一个'中间层', 将这些请求接过来'转发对应微服务模块!'!
      那么这个'中间层 - 网关层'!

      - 2
        注册中心作为'服务注册表', 那么网关层就相当于'协调各个微服务的 - 管家'!

        +----------------------------------+
        |   localhot:10001/user/getName    |                          USER-SERVICE  /user/getName
        |   localhot:10002/order/getOrder  | ======== Gateway ======> ORDER-SERVICE /order/getOrder
        |   localhot:10003/car/getCar      |                          CAR-SERVICE   /car/getCar
        +----------------------------------+

        配置的网关会'断言 - 路由规则', 符合的话会在'对应转发的服务 - 后面拼接'.
        @Bean
        public RouteLocator routes(RouteLocatorBuilder builder) {
            return builder.routes()

                    # 会[截断url第一层], 截断[client-user], 对应转发到微服务[EUREKA-CLIENT]
                    # 示例: /client-user/**         ===> EUREKA-CLIENT/**
                    # 注意: 转发的地址必须在[EUREKA-CLIENT]服务上有该接口地址!
                    .route(r -> r.path("/client-user/**")
                            .filters(f -> f.stripPrefix(1))
                            .uri("lb://EUREKA-CLIENT/"))

                    # 不做截断, 直接将断言到的[/consumer-user/**], [拼接]在微服务 [EUREKA-CONSUMER]后面作为转发路径
                    # 示例: EUREKA-CONSUMER/consumer-user/**
                    .route(r -> r.path("/consumer-user/**")
                            .uri("lb://EUREKA-CONSUMER/"))
                    .build();
        }

        总结: 其实说白了, 就是写'目标微服务的接口地址'...所以这里要绕过来...^-^
             .route(r -> r.path("/controller的RequestMapping的参数地址/各个方法").uri("lb://EUREKA-CLIENT/")
             .route(r -> r.path("/consumer-user/**").uri("lb://EUREKA-CLIENT/")

             @RestController
             @RequestMapping("/consumer-user")
             public class TestApplication {}

        注意: 断言规则较'严格', 多/少 '/' 都会影响转发的准确度! 不对的话会报: 404...
